/*    
 * GPS Averaging
 * Copyright Jeffry Johnston 2009
 *
 * This file is part of GPS Averaging.
 * 
 * GPS Averaging is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GPS Averaging is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GPS Averaging.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.kidsquid.gpsaveraging;

import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;

public class GPSAveraging extends Activity {

  public static final String PREFS_NAME = "GPSAveraging";
  public static final int SETTINGS_DMS = 0;
  public static final int SETTINGS_METRIC = 1;
  public static final double METERS = 1.0;
  public static final double METERS_TO_FEET = 100 / 30.48;
  public static final String DEGREES = "\u00B0";
  public static final String MINUTES = "'";
  public static final String SECONDS = "\"";
  public static final String PREF_DMS = "dms";
  public static final String PREF_METRIC = "metric";
  public static final String PREF_SUM_LATITUDE = "sum_latitude";
  public static final String PREF_FIX_LATITUDE = "fix_latitude";
  public static final String PREF_AVERAGE_LATITUDE = "average_latitude";
  public static final String PREF_LAT_LONG_SAMPLES = "lat_long_samples";
  public static final String PREF_SUM_LONGITUDE = "sum_longitude";
  public static final String PREF_FIX_LONGITUDE = "fix_longitude";
  public static final String PREF_AVERAGE_LONGITUDE = "average_longitude";
  public static final String PREF_SUM_ELEVATION = "sum_elevation";
  public static final String PREF_FIX_ELEVATION = "fix_elevation";
  public static final String PREF_AVERAGE_ELEVATION = "average_elevation";
  public static final String PREF_ELEVATION_SAMPLES = "elevation_samples";
  public static final String SUFFIX_METERS = " meters";
  public static final String SUFFIX_FEET = " feet";
  public static final String UNKNOWN = "Unknown";

  private TextView current_latitude_tv = null;
  private TextView current_longitude_tv = null;
  private TextView current_elevation_tv = null;
  private TextView current_satellites_tv = null;
  private TextView current_date_tv = null;
  private TextView average_latitude_tv = null;
  private TextView average_longitude_tv = null;
  private TextView average_elevation_tv = null;
  private TextView average_samples_tv = null;
  private LocationManager lm = null;
  private LocationListener listener = null;

  private boolean current_exists;
  private double current_latitude;
  private double current_longitude;
  private double current_elevation;
  private Object current_satellites;
  private Date current_date;
  private boolean running = false;
  private boolean running_changed = true;

  private boolean dms = true;
  private boolean metric = false;
  private double average_latitude;
  private double sum_latitude;
  private double fix_latitude;
  private int lat_long_samples;
  private double average_longitude;
  private double sum_longitude;
  private double fix_longitude;
  private double average_elevation;
  private double sum_elevation;
  private double fix_elevation;
  private int elevation_samples;

  // TODO display elapsed runtime d/h/m/s ???
  // TODO menu: create new capture, rename, delete
  // TODO menu: view google map, current or average position
  // TODO settings menu -> change to activity
  // TODO setting: minimum number of satellites needed to collect data
  // TODO setting: minimum DOP needed to collect data
  // TODO setting: GPS sampling frequency

  /** Called when the program is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    current_latitude_tv = (TextView) findViewById(R.current.latitude);
    current_longitude_tv = (TextView) findViewById(R.current.longitude);
    current_elevation_tv = (TextView) findViewById(R.current.elevation);
    current_satellites_tv = (TextView) findViewById(R.current.satellites);
    current_date_tv = (TextView) findViewById(R.current.date);
    average_latitude_tv = (TextView) findViewById(R.average.latitude);
    average_longitude_tv = (TextView) findViewById(R.average.longitude);
    average_elevation_tv = (TextView) findViewById(R.average.elevation);
    average_samples_tv = (TextView) findViewById(R.average.samples);

    setDefaults();
    getPrefs();
    updateDisplay();
  }

  /** User let screen timeout expire or pushed the red disconnect button. */
  @Override
  public void onPause() {
    super.onPause();
    savePrefs();
  }

  /** User backed out of the program. */
  @Override
  public void onStop() {
    super.onStop();
    if ((lm != null) && (listener != null)) {
      lm.removeUpdates(listener);
    }
    savePrefs();
  }

  @Override
  public void onStart() {
    super.onStart();
    startGPS();
  }

  /** Program killed. */
  @Override
  public void onDestroy() {
    super.onDestroy();
    if ((lm != null) && (listener != null)) {
      lm.removeUpdates(listener);
    }
    savePrefs();
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    if (running_changed) {
      MenuInflater inflater = getMenuInflater();
      menu.clear();
      if (running) {
        inflater.inflate(R.layout.menu_started, menu);
      } else {
        inflater.inflate(R.layout.menu_stopped, menu);
      }
      running_changed = false;
    }
    return true;
  }

  /** Handle menu item selection */
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case R.menu.settings:
      final CharSequence[] items = { "Show as D" + DEGREES + "M" + MINUTES + "S" + SECONDS, "Use Metric System" };
      boolean[] settings = { dms, metric };
      AlertDialog.Builder settings_dialog = new AlertDialog.Builder(this);
      settings_dialog.setTitle("Settings");
      settings_dialog.setMultiChoiceItems(items, settings, new DialogInterface.OnMultiChoiceClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int item, boolean isChecked) {
          switch (item) {
          case SETTINGS_DMS:
            dms = isChecked;
            break;
          case SETTINGS_METRIC:
            metric = isChecked;
            break;
          }
          updateDisplay();
        }
      });
      settings_dialog.show();
      break;
    case R.menu.start:
      running = true;
      running_changed = true;
      break;
    case R.menu.stop:
      running = false;
      running_changed = true;
      break;
    case R.menu.clear:
      setDefaults();
      updateDisplay();
      break;
    default:
      return false;
    }
    return true;
  }

  public void startGPS() {
    if (lm == null) {
      lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    }
    if (listener == null) {
      listener = new LocationListener() {

        @Override
        public void onLocationChanged(Location location) {
          updateFields(location);
          updateDisplay();
        }

        @Override
        public void onProviderDisabled(String provider) {
          AlertDialog.Builder gps_dialog = new AlertDialog.Builder(GPSAveraging.this);
          gps_dialog.setMessage("GPS is not enabled.  To enable:\n" + "\u2022 Go to 'Settings'\n" + "\u2022 Select 'Security & location'\n" + "\u2022 Check 'Enable GPS satellites'");
          gps_dialog.setCancelable(false);
          gps_dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
            }
          });
          gps_dialog.show();
        }

        @Override
        public void onProviderEnabled(String provider) {
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
        }

      };
    }
    lm.removeUpdates(listener);
    lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
  }

  public String degreesToString(double raw, boolean latitude) {
    int degrees, minutes;
    char direction;
    if (dms) {
      direction = latitude ? ((raw >= 0) ? 'N' : 'S') : ((raw >= 0) ? 'E' : 'W');
      degrees = (int) Math.abs(raw);
      raw = 60 * (raw - (int) raw);
      minutes = (int) Math.abs(raw);
      raw = Math.abs(60 * (raw - (int) raw));
      return degrees + DEGREES + minutes + MINUTES + raw + SECONDS + direction;
    }
    return raw + DEGREES;
  }

  public void setDefaults() {
    current_exists = false;
    current_elevation = Double.NaN;
    current_satellites = null;
    sum_latitude = 0.0;
    fix_latitude = 0.0;
    average_latitude = 0.0;
    lat_long_samples = 0;
    sum_longitude = 0.0;
    fix_longitude = 0.0;
    average_longitude = 0.0;
    sum_elevation = 0.0;
    fix_elevation = 0.0;
    average_elevation = 0.0;
    elevation_samples = 0;
  }

  public void getPrefs() {
    SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
    dms = prefs.getBoolean(PREF_DMS, dms);
    metric = prefs.getBoolean(PREF_METRIC, metric);
    sum_latitude = Double.longBitsToDouble(prefs.getLong(PREF_SUM_LATITUDE, Double.doubleToLongBits(sum_latitude)));
    fix_latitude = Double.longBitsToDouble(prefs.getLong(PREF_FIX_LATITUDE, Double.doubleToLongBits(fix_latitude)));
    average_latitude = Double.longBitsToDouble(prefs.getLong(PREF_AVERAGE_LATITUDE, Double.doubleToLongBits(average_latitude)));
    lat_long_samples = prefs.getInt(PREF_LAT_LONG_SAMPLES, lat_long_samples);
    sum_longitude = Double.longBitsToDouble(prefs.getLong(PREF_SUM_LONGITUDE, Double.doubleToLongBits(sum_longitude)));
    fix_longitude = Double.longBitsToDouble(prefs.getLong(PREF_FIX_LONGITUDE, Double.doubleToLongBits(fix_longitude)));
    average_longitude = Double.longBitsToDouble(prefs.getLong(PREF_AVERAGE_LONGITUDE, Double.doubleToLongBits(average_longitude)));
    sum_elevation = Double.longBitsToDouble(prefs.getLong(PREF_SUM_ELEVATION, Double.doubleToLongBits(sum_elevation)));
    fix_elevation = Double.longBitsToDouble(prefs.getLong(PREF_FIX_ELEVATION, Double.doubleToLongBits(fix_elevation)));
    average_elevation = Double.longBitsToDouble(prefs.getLong(PREF_AVERAGE_ELEVATION, Double.doubleToLongBits(average_elevation)));
    elevation_samples = prefs.getInt(PREF_ELEVATION_SAMPLES, elevation_samples);
  }

  public void savePrefs() {
    SharedPreferences.Editor prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit();
    prefs.putBoolean(PREF_DMS, dms);
    prefs.putBoolean(PREF_METRIC, metric);
    prefs.putLong(PREF_AVERAGE_LATITUDE, Double.doubleToLongBits(average_latitude));
    prefs.putInt(PREF_LAT_LONG_SAMPLES, lat_long_samples);
    prefs.putLong(PREF_AVERAGE_LONGITUDE, Double.doubleToLongBits(average_longitude));
    prefs.putLong(PREF_AVERAGE_ELEVATION, Double.doubleToLongBits(average_elevation));
    prefs.putInt(PREF_ELEVATION_SAMPLES, elevation_samples);
    prefs.commit();
  }

  public void updateFields(Location location) {
    double y, t;
    current_exists = true;

    /* Current */
    current_date = Calendar.getInstance().getTime();
    current_latitude = location.getLatitude();
    current_longitude = location.getLongitude();
    if (location.hasAltitude()) {
      current_elevation = location.getAltitude();
    }
    if (location.getExtras().containsKey("satellites")) {
      current_satellites = location.getExtras().get("satellites");
    }

    /* Averages */
    /*
     * function kahanSum(input)
     var sum = input[1]
     var c = 0.0          //A running compensation for lost low-order bits.
     for i = 2 to input.length
      y = input[i] - c    //So far, so good: c is zero.
      t = sum + y         //Alas, sum is big, y small, so low-order digits of y are lost.
      c = (t - sum) - y   //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
      sum = t             //Algebraically, c should always be zero. Beware eagerly optimising compilers!
     next i               //Next time around, the lost low part will be added to y in a fresh attempt.
    return sum
     */
    if (running) {
      ++lat_long_samples;
      y = current_latitude - fix_latitude;
      t = sum_latitude + y;
      fix_latitude = (t - sum_latitude) - y;
      sum_latitude = t;
      average_latitude = sum_latitude / lat_long_samples;
      //average_latitude += ((current_latitude - average_latitude) / lat_long_samples);
      y = current_longitude - fix_longitude;
      t = sum_longitude + y;
      fix_longitude = (t - sum_longitude) - y;
      sum_longitude = t;
      average_longitude = sum_longitude / lat_long_samples;
      //average_longitude += ((current_longitude - average_longitude) / lat_long_samples);
      if (location.hasAltitude()) {
        ++elevation_samples;
        y = current_elevation - fix_elevation;
        t = sum_elevation + y;
        fix_elevation = (t - sum_elevation) - y;
        sum_elevation = t;
        average_elevation = sum_elevation / elevation_samples;
        //average_elevation += ((current_elevation - average_elevation) / elevation_samples);
      }
    }
  }

  public void updateDisplay() {
    double conversion;
    String suffix;
    if (metric) {
      conversion = METERS;
      suffix = SUFFIX_METERS;
    } else {
      conversion = METERS_TO_FEET;
      suffix = SUFFIX_FEET;
    }

    /* Display current values */
    if (current_exists) {
      /* Display latitude */
      current_latitude_tv.setText(degreesToString(current_latitude, true));

      /* Display longitude */
      current_longitude_tv.setText(degreesToString(current_longitude, false));

      /* Display elevation */
      if (!Double.isNaN(current_elevation)) {
        current_elevation_tv.setText(String.valueOf(current_elevation * conversion) + suffix);
      } else {
        current_elevation_tv.setText(UNKNOWN);
      }

      /* Display number of satellites */
      if (current_satellites == null) {
        current_satellites_tv.setText(UNKNOWN);
      } else {
        current_satellites_tv.setText(current_satellites.toString());
      }

      /* Display date when current was read */
      current_date_tv.setText(DateFormat.getDateTimeInstance().format(current_date));
    } else {
      current_latitude_tv.setText(UNKNOWN);
      current_longitude_tv.setText(UNKNOWN);
      current_elevation_tv.setText(UNKNOWN);
      current_satellites_tv.setText(UNKNOWN);
    }

    /* Display average values */
    if (lat_long_samples > 0) {
      /* Display latitude */
      average_latitude_tv.setText(degreesToString(average_latitude, true));

      /* Display longitude */
      average_longitude_tv.setText(degreesToString(average_longitude, false));

      /* Display sample counts */
      average_samples_tv.setText(String.valueOf(lat_long_samples));
    } else {
      average_latitude_tv.setText(UNKNOWN);
      average_longitude_tv.setText(UNKNOWN);
      average_samples_tv.setText(UNKNOWN);
    }

    /* Display elevation */
    if (elevation_samples > 0) {
      average_elevation_tv.setText(String.valueOf(average_elevation * conversion) + suffix);
    } else {
      average_elevation_tv.setText(UNKNOWN);
    }
  }
}